<template><h1 id="_33-跨站脚本攻击-xss-为什么cookie中有httponly属性" tabindex="-1"><a class="header-anchor" href="#_33-跨站脚本攻击-xss-为什么cookie中有httponly属性" aria-hidden="true">#</a> 33 | 跨站脚本攻击（XSS）：为什么Cookie中有HttpOnly属性？</h1>
<p>通过<a href="/guide/32" target="_blank" rel="noopener noreferrer">上篇文章<ExternalLinkIcon/></a>的介绍，我们知道了同源策略可以隔离各个站点之间的 DOM 交互、页面数据和网络通信，虽然严格的同源策略会带来更多的安全，但是也束缚了 Web。这就需要在安全和自由之间找到一个平衡点，所以我们默认页面中可以引用任意第三方资源，然后又引入 CSP 策略来加以限制；默认 XMLHttpRequest 和 Fetch 不能跨站请求资源，然后又通过 CORS 策略来支持其跨域。</p>
<p>不过支持页面中的第三方资源引用和 CORS 也带来了很多安全问题，其中最典型的就是 XSS 攻击。</p>
<h2 id="什么是-xss-攻击" tabindex="-1"><a class="header-anchor" href="#什么是-xss-攻击" aria-hidden="true">#</a> 什么是 XSS 攻击</h2>
<p>XSS 全称是 Cross Site Scripting，为了与“CSS”区分开来，故简称 XSS，翻译过来就是“跨站脚本”。XSS 攻击是指黑客往 HTML 文件中或者 DOM 中注入恶意脚本，从而在用户浏览页面时利用注入的恶意脚本对用户实施攻击的一种手段。</p>
<p>最开始的时候，这种攻击是通过跨域来实现的，所以叫“跨域脚本”。但是发展到现在，往 HTML 文件中注入恶意代码的方式越来越多了，所以是否跨域注入脚本已经不是唯一的注入手段了，但是 XSS 这个名字却一直保留至今。</p>
<p>当页面被注入了恶意 JavaScript 脚本时，浏览器无法区分这些脚本是被恶意注入的还是正常的页面内容，所以恶意注入 JavaScript 脚本也拥有所有的脚本权限。下面我们就来看看，如果页面被注入了恶意 JavaScript 脚本，恶意脚本都能做哪些事情。</p>
<ul>
<li>
<p>可以<strong>窃取 Cookie 信息</strong>。恶意 JavaScript 可以通过“document.cookie”获取 Cookie 信息，然后通过 XMLHttpRequest 或者 Fetch 加上 CORS 功能将数据发送给恶意服务器；恶意服务器拿到用户的 Cookie 信息之后，就可以在其他电脑上模拟用户的登录，然后进行转账等操作。</p>
</li>
<li>
<p>可以<strong>监听用户行为</strong>。恶意 JavaScript 可以使用“addEventListener”接口来监听键盘事件，比如可以获取用户输入的信用卡等信息，将其发送到恶意服务器。黑客掌握了这些信息之后，又可以做很多违法的事情。</p>
</li>
<li>
<p>可以通过<strong>修改 DOM</strong> 伪造假的登录窗口，用来欺骗用户输入用户名和密码等信息。</p>
</li>
<li>
<p>还可以<strong>在页面内生成浮窗广告</strong>，这些广告会严重地影响用户体验。</p>
</li>
</ul>
<p>除了以上几种情况外，恶意脚本还能做很多其他的事情，这里就不一一介绍了。总之，如果让页面插入了恶意脚本，那么就相当于把我们页面的隐私数据和行为完全暴露给黑客了。</p>
<h2 id="恶意脚本是怎么注入的" tabindex="-1"><a class="header-anchor" href="#恶意脚本是怎么注入的" aria-hidden="true">#</a> 恶意脚本是怎么注入的</h2>
<p>现在我们知道了页面中被注入恶意的 JavaScript 脚本是一件非常危险的事情，所以网站开发者会尽可能地避免页面中被注入恶意脚本。要想避免站点被注入恶意脚本，就要知道有哪些常见的注入方式。通常情况下，主要有<strong>存储型 XSS 攻击、反射型 XSS 攻击</strong>和<strong>基于 DOM 的 XSS 攻击</strong>三种方式来注入恶意脚本。</p>
<h3 id="_1-存储型-xss-攻击" tabindex="-1"><a class="header-anchor" href="#_1-存储型-xss-攻击" aria-hidden="true">#</a> 1. 存储型 XSS 攻击</h3>
<p>我们先来看看存储型 XSS 攻击是怎么向 HTML 文件中注入恶意脚本的，你可以参考下图：</p>
<p><img src="https://static001.geekbang.org/resource/image/2e/14/2ed3d8b93035df3c2bcfcc223dc47914.png" alt="存储型 XSS 攻击"></p>
<p>通过上图，我们可以看出存储型 XSS 攻击大致需要经过如下步骤：</p>
<ul>
<li>
<p>首先黑客利用站点漏洞将一段恶意 JavaScript 代码提交到网站的数据库中；</p>
</li>
<li>
<p>然后用户向网站请求包含了恶意 JavaScript 脚本的页面；</p>
</li>
<li>
<p>当用户浏览该页面的时候，恶意脚本就会将用户的 Cookie 信息等数据上传到服务器。</p>
</li>
</ul>
<p>下面我们来看个例子，2015 年喜马拉雅就被曝出了存储型 XSS 漏洞。起因是在用户设置专辑名称时，服务器对关键字过滤不严格，比如可以将专辑名称设置为一段 JavaScript，如下图所示：</p>
<p><img src="https://static001.geekbang.org/resource/image/54/49/5479e94a06d9a7cdf3920c60bf834249.png" alt="黑客将恶意代码存储到漏洞服务器上"></p>
<p>当黑客将专辑名称设置为一段 JavaScript 代码并提交时，喜马拉雅的服务器会保存该段 JavaScript 代码到数据库中。然后当用户打开黑客设置的专辑时，这段代码就会在用户的页面里执行（如下图），这样就可以获取用户的 Cookie 等数据信息。</p>
<p><img src="https://static001.geekbang.org/resource/image/ef/3a/efaf4123438f37da4c7366b87ed1403a.png" alt="用户打开了含有恶意脚本的页面"></p>
<p>当用户打开黑客设置的专辑页面时，服务器也会将这段恶意 JavaScript 代码返回给用户，因此这段恶意脚本就在用户的页面中执行了。</p>
<p>恶意脚本可以通过 XMLHttpRequest 或者 Fetch 将用户的 Cookie 数据上传到黑客的服务器，如下图所示：</p>
<p><img src="https://static001.geekbang.org/resource/image/b1/cb/b19300e39a753774f4a94635b46af7cb.png" alt="将 Cookie 等数据上传到黑客服务器"></p>
<p>黑客拿到了用户 Cookie 信息之后，就可以利用 Cookie 信息在其他机器上登录该用户的账号（如下图），并利用用户账号进行一些恶意操作。</p>
<p><img src="https://static001.geekbang.org/resource/image/72/52/720c6daf21a8adf55329af9eaa5ab052.png" alt="黑客利用 Cookie 信息登录用户账户"></p>
<p>以上就是存储型 XSS 攻击的一个典型案例，这是乌云网在 2015 年曝出来的，虽然乌云网由于某些原因被关停了，但是你依然可以通过这个站点来查看乌云网的一些备份信息。</p>
<h3 id="_2-反射型-xss-攻击" tabindex="-1"><a class="header-anchor" href="#_2-反射型-xss-攻击" aria-hidden="true">#</a> 2. 反射型 XSS 攻击</h3>
<p>在一个反射型 XSS 攻击过程中，恶意 JavaScript 脚本属于用户发送给网站请求中的一部分，随后网站又把恶意 JavaScript 脚本返回给用户。当恶意 JavaScript 脚本在用户页面中被执行时，黑客就可以利用该脚本做一些恶意操作。</p>
<p>这样讲有点抽象，下面我们结合一个简单的 Node 服务程序来看看什么是反射型 XSS。首先我们使用 Node 来搭建一个简单的页面环境，搭建好的服务代码如下所示：</p>
<div class="language-javascript ext-js line-numbers-mode"><pre v-pre class="language-javascript"><code><span class="token keyword">var</span> express <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'express'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">var</span> router <span class="token operator">=</span> express<span class="token punctuation">.</span><span class="token function">Router</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>


<span class="token comment">/* GET home page. */</span>
router<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token string">'/'</span><span class="token punctuation">,</span> <span class="token keyword">function</span><span class="token punctuation">(</span><span class="token parameter">req<span class="token punctuation">,</span> res<span class="token punctuation">,</span> next</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
  res<span class="token punctuation">.</span><span class="token function">render</span><span class="token punctuation">(</span><span class="token string">'index'</span><span class="token punctuation">,</span> <span class="token punctuation">{</span> <span class="token literal-property property">title</span><span class="token operator">:</span> <span class="token string">'Express'</span><span class="token punctuation">,</span><span class="token literal-property property">xss</span><span class="token operator">:</span>req<span class="token punctuation">.</span>query<span class="token punctuation">.</span>xss <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

module<span class="token punctuation">.</span>exports <span class="token operator">=</span> router<span class="token punctuation">;</span>
</code></pre><div class="line-numbers" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br></div></div><div class="language-html ext-html line-numbers-mode"><pre v-pre class="language-html"><code><span class="token doctype"><span class="token punctuation">&lt;!</span><span class="token doctype-tag">DOCTYPE</span> <span class="token name">html</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>html</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>head</span><span class="token punctuation">></span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>title</span><span class="token punctuation">></span></span>&lt;%= title %><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>title</span><span class="token punctuation">></span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>link</span> <span class="token attr-name">rel</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">'</span>stylesheet<span class="token punctuation">'</span></span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">'</span>/stylesheets/style.css<span class="token punctuation">'</span></span> <span class="token punctuation">/></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>head</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>body</span><span class="token punctuation">></span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>h1</span><span class="token punctuation">></span></span>&lt;%= title %><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>h1</span><span class="token punctuation">></span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>p</span><span class="token punctuation">></span></span>Welcome to &lt;%= title %><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>p</span><span class="token punctuation">></span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span><span class="token punctuation">></span></span>
      &lt;%- xss %>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>body</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>html</span><span class="token punctuation">></span></span>
</code></pre><div class="line-numbers" aria-hidden="true"><span class="line-number">1</span><br><span class="line-number">2</span><br><span class="line-number">3</span><br><span class="line-number">4</span><br><span class="line-number">5</span><br><span class="line-number">6</span><br><span class="line-number">7</span><br><span class="line-number">8</span><br><span class="line-number">9</span><br><span class="line-number">10</span><br><span class="line-number">11</span><br><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br></div></div><p>上面这两段代码，第一段是路由，第二段是视图，作用是将 URL 中 xss 参数的内容显示在页面。我们可以在本地演示下，比如打开http://localhost:3000/?xss=123这个链接，这样在页面中展示就是“123”了（如下图），是正常的，没有问题的。</p>
<p><img src="https://static001.geekbang.org/resource/image/7e/db/7ecb717abadfff2637a168d39f0c3cdb.png" alt="正常打开页面"></p>
<p>但当打开<code v-pre>http://localhost:3000/?xss=&lt;script&gt;alert('你被xss攻击了')&lt;/script&gt;</code>这段 URL 时，其结果如下图所示：</p>
<p><img src="https://static001.geekbang.org/resource/image/4d/fa/4dff7d83fe2eecc6cb52c126b4f650fa.png" alt="反射型 XSS 攻击"></p>
<p>通过这个操作，我们会发现用户将一段含有恶意代码的请求提交给 Web 服务器，Web 服务器接收到请求时，又将恶意代码反射给了浏览器端，这就是反射型 XSS 攻击。在现实生活中，黑客经常会通过 QQ 群或者邮件等渠道诱导用户去点击这些恶意链接，所以对于一些链接我们一定要慎之又慎。</p>
<p>另外需要注意的是，<strong>Web 服务器不会存储反射型 XSS 攻击的恶意脚本，这是和存储型 XSS 攻击不同的地方</strong>。</p>
<h3 id="_3-基于-dom-的-xss-攻击" tabindex="-1"><a class="header-anchor" href="#_3-基于-dom-的-xss-攻击" aria-hidden="true">#</a> 3. 基于 DOM 的 XSS 攻击</h3>
<p>基于 DOM 的 XSS 攻击是不牵涉到页面 Web 服务器的。具体来讲，黑客通过各种手段将恶意脚本注入用户的页面中，比如通过网络劫持在页面传输过程中修改 HTML 页面的内容，这种劫持类型很多，有通过 WiFi 路由器劫持的，有通过本地恶意软件来劫持的，它们的共同点是在 Web 资源传输过程或者在用户使用页面的过程中修改 Web 页面的数据。</p>
<h2 id="如何阻止-xss-攻击" tabindex="-1"><a class="header-anchor" href="#如何阻止-xss-攻击" aria-hidden="true">#</a> 如何阻止 XSS 攻击</h2>
<p>我们知道存储型 XSS 攻击和反射型 XSS 攻击都是需要经过 Web 服务器来处理的，因此可以认为这两种类型的漏洞是服务端的安全漏洞。而基于 DOM 的 XSS 攻击全部都是在浏览器端完成的，因此基于 DOM 的 XSS 攻击是属于前端的安全漏洞。</p>
<p>但无论是何种类型的 XSS 攻击，它们都有一个共同点，那就是首先往浏览器中注入恶意脚本，然后再通过恶意脚本将用户信息发送至黑客部署的恶意服务器上。</p>
<p>所以要阻止 XSS 攻击，我们可以通过阻止恶意 JavaScript 脚本的注入和恶意消息的发送来实现。</p>
<p>接下来我们就来看看一些常用的阻止 XSS 攻击的策略。</p>
<h3 id="_1-服务器对输入脚本进行过滤或转码" tabindex="-1"><a class="header-anchor" href="#_1-服务器对输入脚本进行过滤或转码" aria-hidden="true">#</a> 1. 服务器对输入脚本进行过滤或转码</h3>
<p>不管是反射型还是存储型 XSS 攻击，我们都可以在服务器端将一些关键的字符进行转码，比如最典型的：</p>
<div class="language-html ext-html line-numbers-mode"><pre v-pre class="language-html"><code>code:<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>script</span><span class="token punctuation">></span></span><span class="token script"><span class="token language-javascript"><span class="token function">alert</span><span class="token punctuation">(</span><span class="token string">'你被xss攻击了'</span><span class="token punctuation">)</span></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>script</span><span class="token punctuation">></span></span>
</code></pre><div class="line-numbers" aria-hidden="true"><span class="line-number">1</span><br></div></div><p>这段代码过滤后，只留下了：</p>
<div class="language-text ext-text line-numbers-mode"><pre v-pre class="language-text"><code>code:
</code></pre><div class="line-numbers" aria-hidden="true"><span class="line-number">1</span><br></div></div><p>这样，当用户再次请求该页面时，由于<code v-pre>&lt;script&gt;</code>标签的内容都被过滤了，所以这段脚本在客户端是不可能被执行的。</p>
<p>除了过滤之外，服务器还可以对这些内容进行转码，还是上面那段代码，经过转码之后，效果如下所示：</p>
<div class="language-text ext-text line-numbers-mode"><pre v-pre class="language-text"><code>code:&amp;lt;script&amp;gt;alert(&amp;#39;你被xss攻击了&amp;#39;)&amp;lt;/script&amp;gt;
</code></pre><div class="line-numbers" aria-hidden="true"><span class="line-number">1</span><br></div></div><p>经过转码之后的内容，如<code v-pre>&lt;script&gt;</code>标签被转换为&lt;script&gt;，因此即使这段脚本返回给页面，页面也不会执行这段脚本。</p>
<h3 id="_2-充分利用-csp" tabindex="-1"><a class="header-anchor" href="#_2-充分利用-csp" aria-hidden="true">#</a> 2. 充分利用 CSP</h3>
<p>虽然在服务器端执行过滤或者转码可以阻止 XSS 攻击的发生，但完全依靠服务器端依然是不够的，我们还需要把 CSP 等策略充分地利用起来，以降低 XSS 攻击带来的风险和后果。</p>
<p>实施严格的 CSP 可以有效地防范 XSS 攻击，具体来讲 CSP 有如下几个功能：</p>
<ul>
<li>
<p>限制加载其他域下的资源文件，这样即使黑客插入了一个 JavaScript 文件，这个 JavaScript 文件也是无法被加载的；</p>
</li>
<li>
<p>禁止向第三方域提交数据，这样用户数据也不会外泄；</p>
</li>
<li>
<p>禁止执行内联脚本和未授权的脚本；</p>
</li>
<li>
<p>还提供了上报机制，这样可以帮助我们尽快发现有哪些 XSS 攻击，以便尽快修复问题。</p>
</li>
</ul>
<p>因此，利用好 CSP 能够有效降低 XSS 攻击的概率。<a href="https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CSP" target="_blank" rel="noopener noreferrer">内容安全策略 - MDN文档<ExternalLinkIcon/></a></p>
<h3 id="_3-使用-httponly-属性" tabindex="-1"><a class="header-anchor" href="#_3-使用-httponly-属性" aria-hidden="true">#</a> 3. 使用 HttpOnly 属性</h3>
<p>由于很多 XSS 攻击都是来盗用 Cookie 的，因此还可以通过使用 HttpOnly 属性来保护我们 Cookie 的安全。</p>
<p>通常服务器可以将某些 Cookie 设置为 HttpOnly 标志，HttpOnly 是服务器通过 HTTP 响应头来设置的，下面是打开 Google 时，HTTP 响应头中的一段：</p>
<div class="language-text ext-text line-numbers-mode"><pre v-pre class="language-text"><code>set-cookie: NID=189=M8q2FtWbsR8RlcldPVt7qkrqR38LmFY9jUxkKo3-4Bi6Qu_ocNOat7nkYZUTzolHjFnwBw0izgsATSI7TZyiiiaV94qGh-BzEYsNVa7TZmjAYTxYTOM9L_-0CN9ipL6cXi8l6-z41asXtm2uEwcOC5oh9djkffOMhWqQrlnCtOI; expires=Sat, 18-Apr-2020 06:52:22 GMT; path=/; domain=.google.com; HttpOnly
</code></pre><div class="line-numbers" aria-hidden="true"><span class="line-number">1</span><br></div></div><p>我们可以看到，set-cookie 属性值最后使用了 HttpOnly 来标记该 Cookie。顾名思义，使用 HttpOnly 标记的 Cookie 只能使用在 HTTP 请求过程中，所以无法通过 JavaScript 来读取这段 Cookie。我们还可以通过 Chrome 开发者工具来查看哪些 Cookie 被标记了 HttpOnly，如下图：</p>
<p><img src="https://static001.geekbang.org/resource/image/de/bb/defa78c90a4e8f0debb09564561ab9bb.png" alt="HttpOnly 演示"></p>
<p>从图中可以看出，<strong>NID 这个 Cookie 的 HttpOlny 属性是被勾选上的，所以 NID 的内容是无法通过 document.cookie 是来读取</strong>的。</p>
<p>由于 JavaScript 无法读取设置了 HttpOnly 的 Cookie 数据，所以即使页面被注入了恶意 JavaScript 脚本，也是无法获取到设置了 HttpOnly 的数据。因此一些比较重要的数据我们建议设置 HttpOnly 标志。</p>
<h2 id="总结" tabindex="-1"><a class="header-anchor" href="#总结" aria-hidden="true">#</a> 总结</h2>
<p>好了，今天我们就介绍到这里，下面我来总结下本文的主要内容。</p>
<p>XSS 攻击就是黑客往页面中注入恶意脚本，然后将页面的一些重要数据上传到恶意服务器。常见的三种 XSS 攻击模式是存储型 XSS 攻击、反射型 XSS 攻击和基于 DOM 的 XSS 攻击。</p>
<p>这三种攻击方式的共同点是都需要往用户的页面中注入恶意脚本，然后再通过恶意脚本将用户数据上传到黑客的恶意服务器上。而三者的不同点在于注入的方式不一样，有通过服务器漏洞来进行注入的，还有在客户端直接注入的。</p>
<p>针对这些 XSS 攻击，主要有三种防范策略，第一种是通过服务器对输入的内容进行过滤或者转码，第二种是充分利用好 CSP，第三种是使用 HttpOnly 来保护重要的 Cookie 信息。</p>
<p>当然除了以上策略之外，我们还可以通过添加验证码防止脚本冒充用户提交危险操作。而对于一些不受信任的输入，还可以限制其输入长度，这样可以增大 XSS 攻击的难度。</p>
<h2 id="思考时间" tabindex="-1"><a class="header-anchor" href="#思考时间" aria-hidden="true">#</a> 思考时间</h2>
<p>今天留给你的思考题是：你认为前端开发者对 XSS 攻击应该负多大责任？</p>
<p>欢迎在留言区与我分享你的想法，也欢迎你在留言区记录你的思考过程。感谢阅读，如果你觉得这篇文章对你有帮助的话，也欢迎把它分享给更多的朋友。</p>
</template>
